# Lurking variable plot for arbitrary covariate.
#
#
# $Revision: 1.65 $ $Date: 2020/12/19 05:25:06 $
#

lurking <- function(object, ...) {
  UseMethod("lurking")
}

lurking.ppp <- lurking.ppm <- local({

  cumsumna <- function(x) { cumsum(ifelse(is.na(x), 0, x)) }

  ## main function
  Lurking.ppm <- function(object, covariate,
                          type="eem",
                          cumulative=TRUE,
                          ..., 
                          plot.it=TRUE,
                          plot.sd=is.poisson(object), 
                          clipwindow=default.clipwindow(object),
                          rv = NULL,
                          envelope=FALSE, nsim=39, nrank=1,
                          typename,
                          covname, oldstyle=FALSE,
                          check=TRUE,
                          verbose=TRUE,
                          nx=128,
                          splineargs=list(spar=0.5),
                          internal=NULL) {
    cl <- match.call()

    ## validate object
    if(is.ppp(object)) {
      X <- object
      object <- ppm(X ~1, forcefit=TRUE)
      dont.complain.about(X)
    } else verifyclass(object, "ppm")

    ## default name for covariate
    if(missing(covname) || is.null(covname)) {
      co <- cl$covariate
      covname <- if(is.name(co)) as.character(co) else
                 if(is.expression(co)) format(co[[1]]) else NULL
    }

    ## handle secret data
    internal <- resolve.defaults(internal,
                                 list(saveworking=FALSE,
                                      Fisher=NULL,
                                      covrange=NULL))
    saveworking <- internal$saveworking
    Fisher      <- internal$Fisher  # possibly from a larger model
    covrange    <- internal$covrange
    
    if(!identical(envelope, FALSE)) {
      ## compute simulation envelope
      Xsim <- NULL
      if(!identical(envelope, TRUE)) {
        ## some kind of object
        Y <- envelope
        if(is.list(Y) && all(sapply(Y, is.ppp))) {
          Xsim <- Y
          envelope <- TRUE
        } else if(inherits(Y, "envelope")) {
          Xsim <- attr(Y, "simpatterns")
          if(is.null(Xsim))
            stop("envelope does not contain simulated point patterns")
          envelope <- TRUE
        } else stop("Unrecognised format of argument: envelope")
        nXsim <- length(Xsim)
        if(missing(nsim) && (nXsim < nsim)) {
          warning(paste("Only", nXsim, "simulated patterns available"))
          nsim <- nXsim
        }
      }
    }
    
    ## may need to refit the model
    if(plot.sd && is.null(getglmfit(object)))
      object <- update(object, forcefit=TRUE, use.internal=TRUE)

    ## match type argument
    type <- pickoption("type", type,
                       c(eem="eem",
                         raw="raw",
                         inverse="inverse",
                         pearson="pearson",
                         Pearson="pearson"))
    if(missing(typename))
      typename <- switch(type,
                         eem="exponential energy weights",
                         raw="raw residuals",
                         inverse="inverse-lambda residuals",
                         pearson="Pearson residuals")

    ## extract spatial locations
    Q <- quad.ppm(object)
    datapoints <- Q$data
    quadpoints <- union.quad(Q)
    Z <- is.data(Q)
    wts <- w.quad(Q)
    ## subset of quadrature points used to fit model
    subQset <- getglmsubset(object)
    if(is.null(subQset)) subQset <- rep.int(TRUE, n.quad(Q))
  
    #################################################################
    ## compute the covariate
    
    if(is.im(covariate)) {
      covvalues <- covariate[quadpoints, drop=FALSE]
      covrange <- covrange %orifnull% range(covariate, finite=TRUE)
    } else if(is.vector(covariate) && is.numeric(covariate)) {
      covvalues <- covariate
      covrange <- covrange %orifnull% range(covariate, finite=TRUE)
      if(length(covvalues) != quadpoints$n)
        stop("Length of covariate vector,", length(covvalues), "!=",
             quadpoints$n, ", number of quadrature points")
    } else if(is.expression(covariate)) {
      ## Expression involving covariates in the model
      glmdata <- getglmdata(object)
      ## Fix special cases
      if(is.null(glmdata)) {
        ## default 
        glmdata <- data.frame(x=quadpoints$x, y=quadpoints$y)
        if(is.marked(quadpoints))
          glmdata$marks <- marks(quadpoints)
      }
      ## ensure x and y are in data frame 
      if(!all(c("x","y") %in% names(glmdata))) {
        glmdata$x <- quadpoints$x
        glmdata$y <- quadpoints$y
      } 
      if(!is.null(object$covariates)) {
        ## Expression may involve an external covariate that's not used in model
        neednames <- all.vars(covariate)
        if(!all(neednames %in% colnames(glmdata))) {
          moredata <- mpl.get.covariates(object$covariates, quadpoints,
                                         covfunargs=object$covfunargs)
          use <- !(names(moredata) %in% colnames(glmdata))
          glmdata <- cbind(glmdata, moredata[,use,drop=FALSE])
        }
      }
      ## Evaluate expression
      sp <- parent.frame()
      covvalues <- eval(covariate, envir= glmdata, enclos=sp)
      covrange <- covrange %orifnull% range(covvalues, finite=TRUE)
      if(!is.numeric(covvalues))
        stop("The evaluated covariate is not numeric")
    } else 
      stop(paste("The", sQuote("covariate"), "should be either",
                 "a pixel image, an expression or a numeric vector"))

    #################################################################
    ## Secret exit
    if(identical(internal$getrange, TRUE))
      return(covrange)
    
    #################################################################
    ## Validate covariate values

    nbg <- is.na(covvalues)
    if(any(offending <- nbg & subQset)) {
      if(is.im(covariate))
        warning(paste(sum(offending), "out of", length(offending),
                      "quadrature points discarded because",
                      ngettext(sum(offending), "it lies", "they lie"),
                      "outside the domain of the covariate image"))
      else
        warning(paste(sum(offending), "out of", length(offending),
                      "covariate values discarded because",
                      ngettext(sum(offending), "it is NA", "they are NA")))
    }
    ## remove points
    ok <- !nbg & subQset
    Q <- Q[ok]
    covvalues <- covvalues[ok]
    quadpoints <- quadpoints[ok]
    ## adjust
    Z <- is.data(Q)
    wts <- w.quad(Q)
    if(any(is.infinite(covvalues) | is.nan(covvalues)))
      stop("covariate contains Inf or NaN values")

    ## Quadrature points marked by covariate value
    covq <- quadpoints %mark% as.numeric(covvalues)

    ################################################################
    ## Residuals/marks attached to appropriate locations.
    ## Stoyan-Grabarnik weights are attached to the data points only.
    ## Others (residuals) are attached to all quadrature points.

    resvalues <- 
      if(!is.null(rv)) rv
      else if(type=="eem") eem(object, check=check)
      else residuals.ppm(object, type=type, check=check)
  
    if(inherits(resvalues, "msr")) {
      ## signed or vector-valued measure
      resvalues <- resvalues$val
      if(ncol(as.matrix(resvalues)) > 1)
        stop("Not implemented for vector measures; use [.msr to split into separate components")
    }

    if(type != "eem")
      resvalues <- resvalues[ok]

    res <- (if(type == "eem") datapoints else quadpoints) %mark% as.numeric(resvalues)

    ## ... and the same locations marked by the covariate
    covres <- if(type == "eem") covq[Z] else covq

    ## NAMES OF THINGS
    ## name of the covariate
    if(is.null(covname)) 
      covname <- if(is.expression(covariate)) covariate else "covariate"
    ## type of residual/mark
    if(missing(typename)) 
      typename <- if(!is.null(rv)) "rv" else ""
    
    #######################################################################
    ## START ANALYSIS
    ## Clip to subwindow if needed
    clip <-
      (!is.poisson.ppm(object) || !missing(clipwindow)) &&
      !is.null(clipwindow)
    if(clip) {
      covq <- covq[clipwindow]
      res <- res[clipwindow]
      covres <- covres[clipwindow]
      clipquad <- inside.owin(quadpoints$x, quadpoints$y, clipwindow)
      wts <- wts[ clipquad ]
    }

    ## -----------------------------------------------------------------------
    ## (A) EMPIRICAL CUMULATIVE FUNCTION
    ## based on data points if type="eem", otherwise on quadrature points

    ## Reorder the data/quad points in order of increasing covariate value
    ## and then compute the cumulative sum of their residuals/marks
    markscovres <- marks(covres)
    o <- fave.order(markscovres)
    covsort <- markscovres[o]
    cummark <- cumsumna(marks(res)[o])
    if(anyDuplicated(covsort)) {
      right <- !duplicated(covsort, fromLast=TRUE)
      covsort <- covsort[right]
      cummark <- cummark[right]
    }
    ## we'll plot(covsort, cummark) in the cumulative case

    ## (B) THEORETICAL MEAN CUMULATIVE FUNCTION
    ## based on all quadrature points
    
    ## Range of covariate values
    covqmarks <- marks(covq)
    covrange <- covrange %orifnull% range(covqmarks, na.rm=TRUE)
    if(diff(covrange) > 0) {
      ## Suitable breakpoints
      cvalues <- seq(from=covrange[1L], to=covrange[2L], length.out=nx)
      csmall <- cvalues[1L] - diff(cvalues[1:2])
      cbreaks <- c(csmall, cvalues)
      ## cumulative area as function of covariate values
      covclass <- cut(covqmarks, breaks=cbreaks)
      increm <- tapply(wts, covclass, sum)
      cumarea <- cumsumna(increm)
    } else {
      ## Covariate is constant
      cvalues <- covrange[1L]
      covclass <- factor(rep(1, length(wts)))
      cumarea <- increm <- sum(wts)
    }
    ## compute theoretical mean (when model is true)
    mean0 <- if(type == "eem") cumarea else numeric(length(cumarea))
    ## we'll plot(cvalues, mean0) in the cumulative case

    ## (A'),(B') DERIVATIVES OF (A) AND (B)
    ##  Required if cumulative=FALSE  
    ##  Estimated by spline smoothing (with x values jittered)
    if(!cumulative) {
      ## fit smoothing spline to (A) 
      ss <- do.call(smooth.spline,
                    append(list(covsort, cummark),
                           splineargs)
                    )
      ## estimate derivative of (A)
      derivmark <- predict(ss, covsort, deriv=1)$y 
      ## similarly for (B) 
      ss <- do.call(smooth.spline,
                    append(list(cvalues, mean0),
                           splineargs)
                    )
      derivmean <- predict(ss, cvalues, deriv=1)$y
    }
  
    ## -----------------------------------------------------------------------
    ## Store what will be plotted
  
    if(cumulative) {
      empirical <- data.frame(covariate=covsort, value=cummark)
      theoretical <- data.frame(covariate=cvalues, mean=mean0)
    } else {
      empirical <- data.frame(covariate=covsort, value=derivmark)
      theoretical <- data.frame(covariate=cvalues, mean=derivmean)
    }

    ## ------------------------------------------------------------------------
  
    ## (C) STANDARD DEVIATION if desired
    ## (currently implemented only for Poisson)
    ## (currently implemented only for cumulative case)

    if(plot.sd && !is.poisson.ppm(object))
      warning(paste("standard deviation is calculated for Poisson model;",
                    "not valid for this model"))

    if(plot.sd && cumulative) {
      ## Fitted intensity at quadrature points
      lambda <- fitted.ppm(object, type="trend", check=check)
      lambda <- lambda[ok]
      ## Fisher information for coefficients
      asymp <- vcov(object,what="internals")
      Fisher <- Fisher %orifnull% asymp$fisher
      ## Local sufficient statistic at quadrature points
      suff <- asymp$suff
      suff <- suff[ok, ,drop=FALSE]
      ## Clip if required
      if(clip) {
        lambda <- lambda[clipquad]
        suff   <- suff[clipquad, , drop=FALSE]  ## suff is a matrix
      }
      ## First term: integral of lambda^(2p+1)
      switch(type,
             pearson={
               varI <- cumarea
             },
             raw={
               ## Compute sum of w*lambda for quadrature points in each interval
               dvar <- tapply(wts * lambda, covclass, sum)
               ## tapply() returns NA when the table is empty
               dvar[is.na(dvar)] <- 0
               ## Cumulate
               varI <- cumsum(dvar)
             },
             inverse=, ## same as eem
             eem={
               ## Compute sum of w/lambda for quadrature points in each interval
               dvar <- tapply(wts / lambda, covclass, sum)
               ## tapply() returns NA when the table is empty
               dvar[is.na(dvar)] <- 0
               ## Cumulate
               varI <- cumsum(dvar)
             })


      ## variance-covariance matrix of coefficients
      V <- try(solve(Fisher), silent=TRUE)
      if(inherits(V, "try-error")) {
        warning("Fisher information is singular; reverting to oldstyle=TRUE")
        oldstyle <- TRUE
      }
      if(any(dim(V) != ncol(suff))) {
        #' drop rows and columns
        nama <- colnames(suff)
        V <- V[nama, nama, drop=FALSE]
      }

      working <- NULL
      
      ## Second term: B' V B
      if(oldstyle) {
        varII <- 0
        if(saveworking) 
          working <- data.frame(varI=varI)
      } else {
        ## lamp = lambda^(p + 1)
        lamp <- switch(type,
                       raw     = lambda, 
                       pearson = sqrt(lambda),
                       inverse =,
                       eem     = as.integer(lambda > 0))
        ## Compute sum of w * lamp * suff for quad points in intervals
        Bcontrib <- as.vector(wts * lamp) * suff
        dB <- matrix(, nrow=length(cumarea), ncol=ncol(Bcontrib),
                     dimnames=list(NULL, colnames(suff)))
        for(j in seq_len(ncol(dB))) 
          dB[,j] <- tapply(Bcontrib[,j], covclass, sum, na.rm=TRUE)
        ## tapply() returns NA when the table is empty
        dB[is.na(dB)] <- 0
        ## Cumulate columns
        B <- apply(dB, 2, cumsum)
        if(!is.matrix(B)) B <- matrix(B, nrow=1)
        ## compute B' V B for each i 
        varII <- quadform(B, V)
        ##  was:   varII <- diag(B %*% V %*% t(B))
        if(saveworking) 
          working <- cbind(data.frame(varI=varI, varII=varII),
                           as.data.frame(B))
      }
      ##
      ## variance of residuals
      varR <- varI - varII
      ## trap numerical errors
      nbg <- (varR < 0)
      if(any(nbg)) {
        ran <- range(varR)
        varR[nbg] <- 0
        relerr <- abs(ran[1L]/ran[2L])
        nerr <- sum(nbg)
        if(relerr > 1e-6) {
          warning(paste(nerr, "negative",
                        ngettext(nerr, "value (", "values (min="),
                        signif(ran[1L], 4), ")",
                        "of residual variance reset to zero",
                        "(out of", length(varR), "values)"))
        }
      }
      theoretical$sd <- sqrt(varR)
    }

    ## 
    if(envelope) {
      ## compute envelopes by simulation
      cl$plot.it <- FALSE
      cl$envelope <- FALSE
      cl$rv <- NULL
      if(is.null(Xsim))
        Xsim <- simulate(object, nsim=nsim, progress=verbose)
      values <- NULL
      if(verbose) {
        cat("Processing.. ")
        state <- list()
      }
      for(i in seq_len(nsim)) {
        cl$object <- update(object, Xsim[[i]])
        result.i <- eval(cl, parent.frame())
        ## interpolate empirical values onto common sequence
        f.i <- with(result.i$empirical,
                    approxfun(covariate, value, rule=2))
        val.i <- f.i(theoretical$covariate)
        values <- cbind(values, val.i)
        if(verbose) state <- progressreport(i, nsim, state=state)
      }
      if(verbose) cat("Done.\n")
      hilo <- if(nrank == 1) apply(values, 1, range) else
                 apply(values, 1, orderstats, k=c(nrank, nsim-nrank+1))
      theoretical$upper <- hilo[1L,]
      theoretical$lower <- hilo[2L,]
    }
    ## ----------------  RETURN COORDINATES ----------------------------
    stuff <- list(empirical=empirical,
                  theoretical=theoretical)
    attr(stuff, "info") <- list(typename=typename,
                                cumulative=cumulative,
                                covrange=covrange,
                                covname=covname,
                                oldstyle=oldstyle)
    if(saveworking) attr(stuff, "working") <- working
    class(stuff) <- "lurk"
    ## ---------------  PLOT THEM  ----------------------------------
    if(plot.it) {
      plot(stuff, ...)
      return(invisible(stuff))
    } else {
      return(stuff)
    }
  }

  Lurking.ppm
})


# plot a lurk object


plot.lurk <- function(x, ..., shade="grey") {
  xplus <- append(x, attr(x, "info"))
  with(xplus, {
    ## work out plot range
    mr <- range(0, empirical$value, theoretical$mean, na.rm=TRUE)
    if(!is.null(theoretical$sd))
      mr <- range(mr,
                  theoretical$mean + 2 * theoretical$sd,
                  theoretical$mean - 2 * theoretical$sd,
                  na.rm=TRUE)
    if(!is.null(theoretical$upper))
      mr <- range(mr, theoretical$upper, theoretical$lower, na.rm=TRUE)

    ## start plot
    vname <- paste(if(cumulative)"cumulative" else "marginal", typename)
    do.call(plot,
            resolve.defaults(
              list(covrange, mr),
              list(type="n"),
              list(...),
              list(xlab=covname, ylab=vname)))
    ## Envelopes
    if(!is.null(theoretical$upper)) {
      Upper <- theoretical$upper
      Lower <- theoretical$lower
    } else if(!is.null(theoretical$sd)) {
      Upper <- with(theoretical, mean+2*sd)
      Lower <- with(theoretical, mean-2*sd)
    } else Upper <- Lower <- NULL
    if(!is.null(Upper) && !is.null(Lower)) {
      xx <- theoretical$covariate
      if(!is.null(shade)) {
        ## shaded envelope region
        shadecol <- if(is.colour(shade)) shade else "grey"
        xx <- c(xx,    rev(xx))
        yy <- c(Upper, rev(Lower))
        dont.complain.about(yy)
        do.call.matched(polygon,
                        resolve.defaults(list(x=quote(xx), y=quote(yy)),
                                         list(...),
                                         list(border=shadecol, col=shadecol)))
      } else {
        do.call(lines,
                resolve.defaults(
                  list(x = quote(xx), y=quote(Upper)),
                  list(...),
                  list(lty=3)))
        do.call(lines,
                resolve.defaults(
                  list(x = quote(xx), y = quote(Lower)),
                  list(...),
                  list(lty=3)))
      }
    }
    ## Empirical
    lines(value ~ covariate, empirical, ...)
    ## Theoretical mean
    do.call(lines,
            resolve.defaults(
              list(mean ~ covariate, quote(theoretical)),
              list(...),
              list(lty=2)))
  })
  return(invisible(NULL))
}

#'  print a lurk object

print.lurk <- function(x, ...) {
  splat("Lurking variable plot (object of class 'lurk')")
  info <- attr(x, "info")
  with(info, {
    splat("Residual type: ", typename)
    splat("Covariate on horizontal axis: ", covname)
    splat("Range of covariate values: ", prange(covrange))
    splat(if(cumulative) "Cumulative" else "Non-cumulative", "plot")
  })
  has.bands <- !is.null(x$theoretical$upper)
  has.sd    <- !is.null(x$theoretical$sd)
  if(!has.bands && !has.sd) {
    splat("No confidence bands computed")
  } else {
    splat("Includes",
          if(has.sd) "standard deviation for" else NULL,
          "confidence bands")
    if(!is.null(info$oldstyle)) 
      splat("Variance calculation:",
            if(info$oldstyle) "old" else "new",
            "style")
  }
  return(invisible(NULL))
}
